Skip to content

fix(ContextMenu): the long press that opens the menu no longer closes it - #7308

Merged
cixzhang merged 4 commits into
facebook:mainfrom
vjeux:context-menu-long-press-lift
Oct 25, 2026
Merged

cixzhang merged 4 commits into
facebook:mainfrom
vjeux:context-menu-long-press-lift

Conversation

@vjeux

@vjeux vjeux commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

User impact

On a phone, a ContextMenu a long press opens closes at once: as the finger lifts, Chromium sends the held element a compatibility mousedown, read as a press outside.

Problem and solution fit

  • Why it occurs: the menu closes on any document mousedown outside it, and a touch's compatibility events target the held trigger.
  • Why it harms: no touch path.
  • Why it matters: iOS opens one only by a long press.

Expected behavior and authority

spec:AST-094 (current) FR33: with a finger or pen, "the long press that opens a ContextMenu MUST NOT close it at the lift". component:ContextMenu FR5 routes the long-press lift there, and architecture:interaction-modality (current) INV4 asks for an operable path per modality. This PR implements that clause only; FR33's outside press (FR32), lift and handoff stay with their own work, and it changes no record row. #7303 proposes FR10 (focus on open); #7840, FR12. Overlap search over open PRs and records (component:ContextMenu, layer-runtime.md; "outside press", "long press", "compatibility"): #7303 and #7840 touch the same listener.

Smallest restoration

main's outside mousedown listener stays, shadow-root handling included. It skips the compatibility mouse events of the finger press that opened the menu: the long press, or a touch contextmenu where Chromium sends one, recorded by the shared gesture counter (Layer/gestureCounter), which advances on every pointerdown and keydown. Any later press is a new gesture, so every other outside press keeps its outcome: a mouse or pen closes at the press, a tap on its compatibility mousedown, while a scroll, a cancelled or stopped pointerdown, an unpanned drag or a second finger leaves it open. A mouse or keyboard open records no gesture. No listener, API or default is added; architecture:layer-runtime names the skip.

Evidence

  • Before, in real Chromium (CDP touch): the menu opens at about 510 ms; on lift come pointerup, touchend, then trusted mousedown, mouseup and click on the trigger, and it closes.
  • After: the menu stays open through that click, in a shadow root too; a tap or mouse click outside still closes it.
  • iOS Safari reportedly sends the mousedown mid-hold; the same gesture is skipped, but WebKit was not run here.
  • Regression proof: the long-press, touch contextmenu and moved-menu unit cases and both Chromium long-press cases fail on main; the mouse, pen and no-mousedown cases fail on the previous head and pass on main.

Writing rubric results

R1 narration: the layer-runtime line is present tense.
R2 research: none.
R3 evidence: none in the record; the tests below verify the line.
R4 rejected alternatives: none.
R5 change references: none.
R6 open questions: none.

Scope

  • One defect is restored.
  • Separable contradictory or unsettled tagalongs were removed or split.
  • No new public API, visual representation, default, or interaction model is hidden under the bug-fix label.
  • Public text and artifacts contain no internal Meta context.

Testing

  • Maintainer merge 690291d brought in main: ContextMenu.spec.md takes main's table, whose FR5 already routes the long-press lift to spec:AST-094 FR33, so this PR no longer proposes FR11; the changeset names only the Chromium lift the tests show. The code, tests and layer-runtime.md line are unchanged.
  • Review round (163af16, rebased onto main 8b74911): narrowed to the long-press lift per the owner's requested change. main's outside mousedown listener returns with one skip, the opening finger press's compatibility events (shared gesture counter); the pointer listeners, the nested-root levels and the five other outcome changes are gone. FR11, layer-runtime.md and the changeset say only that. ContextMenu, DropdownMenu, hooks, Layer and CLI suites: 1422 tests; long-press, shadow-root and ContextMenu audit Chromium specs: 23; native types, typecheck, eslint (strict), check:knowledge and the Storybook build pass.
  • Rebased onto main 39b5981 (486cec8), patch unchanged: the typecheck job's native platform types step runs a script main added in ci(native): typecheck the native package and every platform type it emits #8024, which 1fe5329's older base lacked. ContextMenu, DropdownMenu, hooks and CLI suites: 1216 tests; the long-press and shadow-root Chromium specs: 5; the native typecheck, build and platform-types steps, typecheck, eslint (strict), check:knowledge and the Storybook build pass.
  • Review round (1fe5329): the document pointerup/pointercancel listeners come on with an outside finger or pen press and off once none is live, instead of staying for the whole open menu (architecture:react-component-runtime INV5); layer-runtime.md says so. A new case shows none attached while the menu is open at rest and none after the lift; it fails on the previous head. ContextMenu, DropdownMenu, hooks and CLI suites: 1151 tests; the long-press and shadow-root Chromium specs: 5; typecheck, eslint (strict), check:knowledge and the Storybook build pass.
Rebased onto main 9f3ab054c0 (a5784a5fa4, one commit): #7914 judged presses inside a shadow root with a `mousedown` listener on the menu's root, run before the document's. Capture reaches the document first, so each root from the document down to the menu's own now listens in the capture phase and defers a press on the host of the next root down; a press on such a host itself is judged by that host, whatever the host's own listeners do. #7914's open, closed and nested cases fire pointer events and also lift a finger on the menu; a new case stops presses in each root and on a host. They fail with the document judging every press (#7914's cases), with roots listening as the press bubbles, and with only the menu's own root listening (the new case). The `a11y/Menu press in a shadow root` stories gain a ContextMenu; the shadow-root spec checks in Chromium that a clicked item is picked (red with the document judging every press), that a press or a tap on the host closes the menu, and that its opening long press does not. `layer-runtime.md` names the extra listeners.
  vitest ContextMenu, DropdownMenu, Selector, hooks, Breadcrumbs   Tests 1193 passed
  playwright MenuPressShadowRoot, ContextMenuLongPress, ContextMenuAudit --project=chromium   23 passed
  typecheck, lint:strict, check:knowledge, storybook:build   ✓

Review round 3: rebased onto main 95e1674ce1 as one commit; the rule is FR11, since #7303 proposes FR10 in the same record:
  vitest ContextMenu, DropdownMenu, useLongPress, docProp*   Tests 376 passed
  playwright ContextMenu/__tests__ --project=chromium   23 passed
  typecheck, lint:strict, check:knowledge, storybook:build   ✓

Review round 2: the listeners run in the capture phase, so a control that stops the press (ResizeHandle, Table's column-resize handle) still closes the menu; new case "closes on a press whose control stops it" fails with bubble-phase listeners:
  vitest ContextMenu, DropdownMenu, useLongPress, docProp*   Tests 374 passed (DropdownMenuLazySheet's import hook timed out at 10 s under load; 2 passed alone)
  playwright ContextMenu/__tests__ --project=chromium   23 passed
  typecheck, lint:strict, check:knowledge, storybook:build   ✓

Review round: the three changed presses are named (changeset, body, commit message), a new case pins the cancelled press, the unpanned drag and a second finger's tap (it fails on `main`), and docs/architecture/layer-runtime.md names the new listeners:
  vitest ContextMenu, DropdownMenu, useLongPress, docProp*   Tests 375 passed
  playwright ContextMenu/__tests__ --project=chromium   23 passed
  typecheck, lint:strict, check:knowledge, storybook:build   ✓

Rebased onto main 22c592a7a5 as one commit. Main's record now has FR9 (overlay dismissal), so this rule was proposed FR10 (now FR11, beside #7303's FR10):
  vitest ContextMenu, DropdownMenu, hooks, docProp*   Tests 854 passed
  playwright ContextMenu/__tests__ --project=chromium   23 passed
  typecheck, lint:strict, check:knowledge, storybook:build   ✓
  red on main: `ContextMenuLongPress.a11y.chromium.spec.ts` against a `main` build: 1 failed (the long press and lift), 1 passed (the guard)

pnpm exec vitest run packages/core/src/ContextMenu/ContextMenu.test.tsx -t "ContextMenu outside dismissal"
  main's ContextMenu.tsx: 3 failed, 3 passed (tap/scroll, mouse and mid-tap re-render guards) · with the change: 6 passed
  (the pen case fails on main, where a pen closes it at once; the re-render case failed on an earlier head)

pnpm exec vitest run packages/core/src/ContextMenu packages/core/src/DropdownMenu packages/core/src/hooks/useLongPress.test.tsx \
  packages/core/src/hooks/useMenuPress.test.tsx packages/core/src/docPropLiterals.test.ts \
  packages/core/src/docPropReferences.test.ts --maxWorkers=8
  Test Files  9 passed (9)   Tests  324 passed (324)

pnpm storybook:build   (this branch; main for the red arm)
ASTRYX_STORYBOOK_DIR=<main build> pnpm exec playwright test \
  packages/core/src/ContextMenu/__tests__/ContextMenuLongPress.a11y.chromium.spec.ts --project=chromium
  1 failed, 1 passed   (long press: the menu is gone after the lift's click; mouse guard passes)
pnpm exec playwright test packages/core/src/ContextMenu/__tests__/ContextMenuLongPress.a11y.chromium.spec.ts --project=chromium
  2 passed · --repeat-each=2: 4 passed

pnpm -F @astryxdesign/core typecheck, pnpm lint:strict, check:knowledge   ✓

@vercel

vercel Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
astryx Ready Ready Preview Oct 25, 2026 9:24pm UTC

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 10, 2026
@astracat-bot

astracat-bot Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Oct 25, 2026, 10:02 PM UTC

@github-actions

github-actions Bot commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

📚 Storybook Preview

View Storybook for this PR

🧪 Sandbox Preview

View Sandbox for this PR

Modified Components

ContextMenu (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Entry File (ESM) 1.1KB 1.1KB 0B
Lines of Code 639 649 -
Complexity Very High Very High (86) -

Component Size

Component Import before Import after Delta CSS before CSS after Delta
core/ContextMenu 76.7KB 76.7KB +42B 14.2KB 14.2KB 0B
core/Markdown 136.0KB 136.0KB +35B 16.9KB 16.9KB 0B
core/Table 82.8KB 82.9KB +27B 14.6KB 14.6KB 0B
core/TableCell 77.7KB 77.8KB +64B 14.3KB 14.3KB 0B
core/TableHeaderCell 77.5KB 77.6KB +60B 14.3KB 14.3KB 0B

Sizes are gzip. Import counts the JavaScript the import loads eagerly; CSS counts the package stylesheet rules referenced by the modules it can load from its own package, lazily loaded ones included.

Bundle Size Summary

Gzip figures measure each package's entry file, not the code an import pulls in.

Package Entry (ESM) Entry gzip before Entry gzip after Delta
@astryxdesign/core 4.9KB 1.2KB 1.2KB 0B
Package stylesheet Gzip before Gzip after Delta
@astryxdesign/core 33.8KB 33.8KB 0B

Accessibility Audit

Status: No accessibility violations detected.

Visual Regression

48 of 502 shot(s) changed.

A change here is a question, not a failure: check whether the after is the
picture you intended. Record that review on the PR. Baseline maintenance is an
explicit dispatch of CI; this report never rewrites the baseline or adds a release gate.

component story theme mode pixels
Selector Indicator Space Bottom Sheet Narrow End RTL neutral light 753,933
Selector Indicator Space Bottom Sheet Wide Start neutral light 740,072
RichTextEditor Markdown parity: side by side probe dark 556,900
RichTextEditor Markdown parity: side by side probe light 538,816
Chat Full AI Chat probe light 230,823
Chat Full AI Chat probe dark 229,805
AppShell With Mobile Nav probe light 217,010
AppShell With Mobile Nav probe dark 216,960
Collapsible Dividers probe dark 66,445
Collapsible Dividers probe light 66,445
AppShell Playground neutral dark 65,198
AppShell Playground neutral light 63,788
Chat Default neutral dark 36,304
Chat Default neutral light 35,767
Chat Empty State probe light 23,531
Chat Empty State probe dark 22,622
ChatAutoScroll Scroll Behavior Comparison neutral dark 15,378
ChatAutoScroll Scroll Behavior Comparison neutral light 14,107
Chat With Attachments probe light 13,406
Chat With Attachments probe dark 11,232

and 28 more.


Generated by PR Enrichment workflow | Storybook | Sandbox | View full report

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fix looks ready to approve. Moving outside dismissal from mousedown to pointer events stops a touch's compatibility mouse events from closing the menu its own long press opened, while keeping the existing outcomes that a mouse press outside closes at once, a finger tap outside closes, and a scroll starting outside does not.

Evidence checked in review:

  • In real Chromium with synthesized touch on the Default story, the previous behavior closed the menu right after the finger lifted, this change leaves it open through the compatibility click, and a later tap outside still closes it.
  • The full ContextMenu unit file passes, including the new long-press, tap/scroll, and mouse outside-dismissal cases, and the core typecheck is clean.

One small non-blocking follow-up: the layer-runtime dismissal inventory still describes ContextMenu as using a local outside mousedown listener, so that one word could be refreshed to match the new pointer-press mechanism whenever convenient.

WebKit was not run here, so the iOS timing rests on the removal of the mousedown listener itself rather than an iOS run.

Thank you for the focused restoration and the real-engine regression coverage.

[Automated review]

@cixzhang cixzhang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CodeBunny approved at this head; CI green. Approved.

@cixzhang
cixzhang enabled auto-merge (squash) October 11, 2026 17:14
auto-merge was automatically disabled October 11, 2026 17:31

Head branch was pushed to by a user without write access

@vjeux
vjeux force-pushed the context-menu-long-press-lift branch from 67b9966 to b0f51c5 Compare October 11, 2026 17:31

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the focused fix: in real Chromium the long-pressed menu now stays open through the lift, and taps, scrolls, and mouse or pen presses outside behave as before. One regression needs fixing first: the outside-dismissal effect depends on closeMenu, whose identity follows onOpenChange, and its cleanup clears outsideTouchesRef, so when a caller passes an inline onOpenChange (as LazyTableContextMenu does) and re-renders between a finger's pointerdown and pointerup, the tap outside no longer closes the menu. Please keep the recorded touches across re-renders (for example, read closeMenu through a ref so the effect depends only on isOpen and usesBottomSheet) and add a test with an inline onOpenChange and a re-render mid-tap.

[Automated review]

Comment thread packages/core/src/ContextMenu/ContextMenu.tsx Outdated

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fix itself checks out: in real Chromium a long-pressed ContextMenu now stays open through the finger's lift, and taps, scrolls, and mouse or pen presses outside still behave as before. Approval now waits on a maintainer decision, because the new FR9 adds a normative outside-press rule to the draft ContextMenu spec that no approved spec states yet, so the component owner needs to approve or adjust it, including whether a pen press should close the menu at once like a mouse, since the menu press model's current spec treats a pen like a finger. Separately, the changeset describes the iOS mid-hold case as fixed without an iOS Safari run; please either add that evidence or limit the note to the Chromium lift the tests prove.

[Automated review]

Comment thread .changeset/context-menu-long-press-lift.md Outdated
Comment thread packages/core/src/ContextMenu/ContextMenu.spec.md Outdated

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The long-press fix itself checks out: in real Chromium the menu now stays open as the finger lifts (it closes on main), and mouse presses, finger taps and drags outside behave as before. Approval waits on a maintainer decision, because FR9 adds a normative outside-press rule to the draft ContextMenu spec that no approved spec states yet; the component owner needs to approve or adjust it, including whether a pen press outside should wait for its lift (as FR9 now says) or keep closing the menu at press as it does today. Two small documentation fixes are also needed:

  • docs/architecture/layer-runtime.md still says ContextMenu uses a local outside mousedown listener.
  • The Changeset and description should note that a press on a control that cancels its pointerdown now closes the menu, which it did not before.

[Automated review]

Comment thread packages/core/src/ContextMenu/ContextMenu.spec.md Outdated
Comment thread packages/core/src/ContextMenu/ContextMenu.tsx Outdated
@vjeux
vjeux force-pushed the context-menu-long-press-lift branch from a065c3c to a14fd1f Compare October 11, 2026 21:37
@vjeux

vjeux commented Oct 11, 2026

Copy link
Copy Markdown
Contributor Author

metadata-only rewrite: commit author identity; tree unchanged

The new head a14fd1f has the same tree as the previous head a065c3c (4525915776).

@astracat-bot astracat-bot Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The long-press fix checks out: in real Chromium the menu now stays open as the finger lifts (it closes on main), while mouse presses, finger taps and pans outside end as before. Approval waits on the ContextMenu owner approving or adjusting the new FR9 outside-press rule, which also makes a pen press close the menu on its lift rather than at press, and makes presses on controls that cancel pointerdown and drags on touch-action: none surfaces close it. Please also:

  • Name those two new dismissals in the Changeset and description, or guard them if the owner keeps main's outcome.
  • Update docs/architecture/layer-runtime.md, which still says ContextMenu uses an outside mousedown listener.
  • Rebase onto main, where the draft ContextMenu record already uses FR9 for shared-stack Escape, and give the new rule the next free number.

[Automated review]

Comment thread packages/core/src/ContextMenu/ContextMenu.spec.md Outdated
Comment thread packages/core/src/ContextMenu/ContextMenu.tsx Outdated

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The long-press fix checks out: in real Chromium the menu now stays open as the finger lifts (it closes on main), while mouse presses, finger taps and pans outside end as before. Approval waits on the ContextMenu owner approving or adjusting the new FR10 outside-press rule, which also makes a pen press close the menu on its lift rather than at press, and makes presses on controls that cancel pointerdown (such as NumberInput's steppers) and drags on touch-action: none surfaces (such as Slider) close it. Please also:

  • Name those two new dismissals in the Changeset and description, or guard them if the owner keeps main's outcome.
  • Update docs/architecture/layer-runtime.md, which still says ContextMenu uses an outside mousedown listener.

[Automated review]

Comment thread packages/core/src/ContextMenu/ContextMenu.spec.md Outdated
Comment thread .changeset/context-menu-long-press-lift.md Outdated
@vjeux
vjeux force-pushed the context-menu-long-press-lift branch from 34397ae to 53fa96a Compare October 20, 2026 15:11

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The long-press fix works: in Chromium the menu now stays open as the finger lifts (it closes on main), while mouse presses, finger taps and pans outside end as before. Approval waits on the ContextMenu owner, as the maintainer for this record, approving or narrowing draft FR11, which also changes five outside presses: a pen closes the menu on its lift, and presses on controls that cancel or stop pointerdown (NumberInput steppers, ResizeHandle, Table column resize), unpanned finger drags on a Slider and second-finger taps now close it. This head also needs an exact-head owner approval for the layer-runtime.md edit, since the earlier /approve-spec named a previous head and was revoked. Optionally, the description could name the reproduced case as a lift the browser reads as a tap, since a lift Chromium reads as its own long press does not close the menu on main either.

[Automated review]

@cixzhang

Copy link
Copy Markdown
Contributor

Posted by Cindy's agent on her behalf.

Parts of this PR re-implement behavior that already exists, or is being specified, as a shared part of core. Converging keeps one implementation per behavior:

This is about where the behavior lives, not about the PR's intent.

@vjeux

vjeux commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor Author

Agreed: one implementation.

Following a press to its release. In 486cec8 the document pointerup/pointercancel listeners are attached only while an outside finger or pen press is live, not for the whole open menu, with a test that none are attached at rest; they move onto the shared press-to-release tracker when that exists.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The long-press fix works: in Chromium a ContextMenu opened by a touch long press now stays open as the finger lifts (it closes on main), while mouse presses, finger taps and pans outside end as before. Approval waits on an owner decision on draft FR11 in ContextMenu.spec.md, which also changes five outside presses: a pen closes the menu on its lift, and presses on controls that cancel or stop pointerdown (NumberInput steppers, resize handles), finger drags on a Slider and second-finger taps now close it. An engineering owner settles this by approving FR11 as written, or naming the outcomes that should keep main's behavior, and recording it with a comment of /approve-spec <full head SHA> on this pull request; the author may do so as an engineering owner, and the same approval covers the layer-runtime.md line. Nothing else in the change needs work.

[Automated review]

@cixzhang

cixzhang commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Posted by Cindy's agent on her behalf.

Requested changes before approval.

  1. Keep the long-press-lift fix, and narrow FR11 to just that. Drop the other five outcome changes, including mouse presses on stepper and resize controls now closing the menu.
  2. The finger and pen outside-press rule lives in spec:AST-094 (being added through spec(AST-094): touch presentations feel native by default #8316), and ContextMenu's record points to it.

vjeux added 3 commits October 24, 2026 21:17
The menu closed on any document mousedown outside it, and a touch's
compatibility mousedown (sent as the finger lifts in Chromium, reported
mid-hold on iOS Safari) lands on the trigger, so the opening long press
dismissed its own menu. Outside dismissal now reads pointer events: a
mouse press outside closes at once, a finger's or a pen's tap outside
closes it on the lift, and a scroll that starts outside still leaves it
open.

Some outside presses change: a pen now closes it on its lift rather
than at the press, and presses the mousedown listener never saw now
close it: a press on a control that cancels its pointerdown, and, on
the lift, a finger drag on a touch-action: none surface or a second
finger's tap.

Proposed FR11 of component:ContextMenu records the rule (another pull
request proposes FR10, where focus lands on open), and the layer
runtime record names the new listeners.

Outside presses are read in the capture phase, so a control that stops
the press (a resize handle) still closes the menu.

Inside a shadow root, even a closed or nested one, the document sees a
press on the menu as a press on a shadow host. Each root from the
document down to the menu's own listens in the capture phase and defers
a press on the host of the next root down to that root, and a press on
such a host itself is judged by that host, so a press on the menu never
closes it and a stopped press outside still does. A story renders the
menu in a shadow root; the shadow-root spec checks in Chromium that a
clicked item is picked, that a press or a tap on the host closes the
menu, and that its opening long press does not.
…le an outside finger or pen press is live

The open menu held document pointerup and pointercancel listeners for its whole open lifetime, though they only matter while a finger's or a pen's outside press is in flight. They now come on with the first such press and off once none is left (a lift, a cancel, or the menu closing), so an open menu with no press in flight holds none (architecture:react-component-runtime INV5); they move onto the shared press-to-release tracker once one exists. The pointerdown listeners that judge a press stay for the open menu, since they are what sees a press begin. layer-runtime.md says so. New case: an open menu holds no document lift listener until a finger presses outside, and drops it with that lift; it fails on the previous head. Every outside-dismissal case still passes.
…side presses keep their outcomes

Per the owner's requested change, the pull request keeps only the long-press-lift fix. Outside dismissal is main's outside mousedown listener again, with one addition: the compatibility mouse events of the finger press that opened the menu (the long press, or a touch contextmenu where Chromium sends one) are skipped, identified by the shared gesture counter (Layer/gestureCounter), which advances on every pointerdown and keydown; any later mousedown outside closes the menu as on main. A mouse or keyboard open records no gesture, so nothing changes for them. The five other outcome changes are dropped: a pen closes at its press again, and presses that send no mousedown (a control that cancels or stops its pointerdown, such as a NumberInput stepper or a resize handle, a finger drag on a surface that does not pan, a second finger) leave the menu open again. The document pointerdown, pointerup and pointercancel listeners and the nested-root levels are gone, so the open menu adds no listener beyond main's. FR11 states only the long-press rule and points finger and pen outside presses to spec:AST-094; layer-runtime.md and the changeset say the same. Tests: the long press (mid-hold and lift, then a later press closes), a touch contextmenu, and a long press that moves the open menu fail on main; a mouse or pen press closing at the press, and the presses that send no mousedown leaving the menu open, fail on the previous head. The long-press and shadow-root Chromium cases fail on main and pass here.
@vjeux
vjeux force-pushed the context-menu-long-press-lift branch from 486cec8 to 163af16 Compare October 25, 2026 04:24
@vjeux

vjeux commented Oct 25, 2026

Copy link
Copy Markdown
Contributor Author

Agreed.

1. Narrow FR11. In 163af16 FR11 covers only the long-press lift: main's outside mousedown listener is back and skips only the opening finger press's compatibility events (the shared gesture counter); a pen, a stopped or cancelled press, an unpanned drag and a second finger keep main's outcomes.
2. Finger and pen rule. FR11 points finger and pen outside presses to spec:AST-094; ContextMenu keeps only the mouse listener and the long-press skip.

@astracat-bot astracat-bot Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The long-press fix works: in Chromium, a ContextMenu opened by a touch long press now stays open as the finger lifts (on main the lift's compatibility mousedown closes it), and mouse, keyboard and tap-outside behavior is unchanged. Since this head was pushed, spec:AST-094 became current on main and its FR33 states this rule, with main's ContextMenu FR5 already pointing to it, so an engineering owner needs to decide whether draft FR11 (finger only, plus a "mid-hold" clause) stays beside FR5 or is dropped, and record that decision by commenting /approve-spec <full head SHA> on the final head; the author may record it too, as an engineering owner. Two fixes are needed either way:

  • Rebase onto main: the FR table in ContextMenu.spec.md now conflicts with the FR5 change from #8409.
  • The changeset's "while it is held on iOS" needs iOS Safari evidence, or should describe only the Chromium lift the tests show.

[Automated review]

ContextMenu.spec.md takes main's table: main's FR5 already routes the long
press that opens the menu to spec:AST-094 FR33, so the draft FR11 (and its
verification row) leaves. The changeset describes only the Chromium lift the
tests show.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The long-press fix is ready: in Chromium a ContextMenu opened by a finger's long press now stays open as the finger lifts (on main the lift's compatibility mousedown closes it), and mouse, keyboard and other outside presses behave as before. Because the pull request edits docs/architecture/layer-runtime.md, a current record, merging still waits on an engineering owner commenting /approve-spec <full head SHA>; the author may record it as an engineering owner. Optionally, the new case titled "a scroll that starts outside leaves it open" could say that it pins today's outcome until spec:AST-094 FR32 is implemented.

[Automated review]

expect(onOpenChange).not.toHaveBeenCalledWith(false);
});

it('a tap outside closes the menu through its compatibility mousedown; a scroll that starts outside leaves it open', () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: this case pins today's outcome for a scroll that starts outside. spec:AST-094 FR32 (current, not yet implemented) says the menu "MUST close as a touch outside it begins to pan", so the FR32 work will have to invert it; saying so in the title or a comment keeps it from reading as the rule.

@cixzhang

Copy link
Copy Markdown
Contributor

/approve-spec 690291d

@github-actions github-actions Bot removed the needs:spec-owner-review Current knowledge records await owner approval label Oct 25, 2026
@cixzhang
cixzhang merged commit caa1f04 into facebook:main Oct 25, 2026
69 checks passed
@vjeux
vjeux deleted the context-menu-long-press-lift branch October 25, 2026 22:25
cixzhang added a commit to vjeux/astryx that referenced this pull request Oct 25, 2026

This branch was successfully deployed

1 active deployment
Preview — 690291d0 Deployed Oct 25, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants